#!/usr/bin/env python3
'''
完成适配

训练记录：
在主力机上训练：
20250327：测试分数248.5，训练分数296.230，分数貌似一直上升
20250328：学习率：5e-4，测试分数248.5分，训练分数 8.650，貌似继续训练存在问题吗？停止训练排查代码
20250329: 
重新在2号机上训练
20250330:在2号机上继续训练感觉比较正常，测试分数249.5，训练分数 317.910，继续训练
20250331：学习率5e-4，测试分数249.5，训练分数-7.343，恢复模型后再继续训练一天看看，感觉kan对持续训练不行，如果是该如何改进？保存缓冲区？
20250401:学习率5e-4，继续训练存在问题，且存在大量的连续Test reward，貌似测试太多了？感觉kan对强化学习中有效，但是必须要连续训练，感觉得包一个在线平台试试
20250402：计划重新训练
20250403：在2号机上重新训练，学习率5e-4，测试分数10.0，训练分数56.640，继续训练试试
20250404：learning rate: 0.0005、Learning Rate: 0.0005，测试分数10.0，训练分数-7.400,停止训练
20250405：从趋势上来说是可以训练的，就是中断后无法继续训练，后续打算继续在在线平台上训练，看看效果如何
20250406:对比加不加激活函数的区别
20250506: 在腾讯云上训练，测试分数272分，训练分数330.340分，继续训练
20250507：在腾讯云上训练，测试分数1962分，训练分数位置，不过估计在1000分以上，表明是可以训练的,备份模型，尝试继续训练
20250508: 在腾讯云上训练，测试分数1962分，新增加了比较多的高分，训练分数1208.230分，继续训练
20250509: 在腾讯云上训练，测试分数2419分，整体测试分数都已经达到2000+分，某种程度上是可以继续训练的，比如两次以内？继续训练
20250519: 在腾讯云上训练，由于超时了，未知分数是多少，等待下个月揭晓
20250603: 在腾讯云上训练，测试分数3694份，训练分数未知，停止训练，play模型，可以继续训练，不过继续训练的次数要少，并且继续训练要更长时间
'''
import gymnasium as gym
import ptan
import numpy as np
import argparse
from tensorboardX import SummaryWriter
import os

import torch
import torch.nn as nn
import torch.nn.utils as nn_utils
import torch.nn.functional as F
import torch.optim as optim

from typing import Any
from lib import common, model
from collections import deque
import ale_py

gym.register_envs(ale_py)

GAMMA = 0.99
LEARNING_RATE = 5e-4
ENTROPY_BETA = 0.01
BATCH_SIZE = 128
NUM_ENVS = 50

REWARD_STEPS = 4
CLIP_GRAD = 0.5

SAVE_ITERS = 100


def unpack_batch(batch, net, device='cpu'):
    """
    Convert batch into training tensors
    :param batch:
    :param net:
    :return: states variable, actions tensor, reference values variable
    """
    states = []
    actions = []
    rewards = []
    not_done_idx = [] # 非结束的游戏数据索引，该索引记录对应batch，states，actions，rewards
    last_states = [] # 记录采样中的执行动作后的状态，仅记录游戏非结束状态下的索引
    for idx, exp in enumerate(batch):
        states.append(np.asarray(exp.state))
        actions.append(int(exp.action))
        rewards.append(exp.reward)
        if exp.last_state is not None:
            not_done_idx.append(idx)
            last_states.append(np.asarray(exp.last_state))
    states_v = torch.FloatTensor(np.asarray(states)).to(device)
    actions_t = torch.LongTensor(actions).to(device)
    rewards_np = np.array(rewards, dtype=np.float32)
    if not_done_idx:
        last_states_v = torch.FloatTensor(np.asarray(last_states)).to(device)
        last_vals_v = net(last_states_v)[1]
        last_vals_np = last_vals_v.data.cpu().numpy()[:, 0]
        rewards_np[not_done_idx] += GAMMA ** REWARD_STEPS * last_vals_np

    ref_vals_v = torch.FloatTensor(rewards_np).to(device)   
    return states_v, actions_t, ref_vals_v


def test_model(env, net, device, episodes=5):
    with torch.no_grad():
        total_reward = 0.0
        for _ in range(episodes):
            noop_action_count = 0
            pre_action = -1
            obs, _ = env.reset()
            while True:
                obs_v = ptan.agent.default_states_preprocessor([obs]).to(device)
                logits_v, _ = net(obs_v)
                probs_v = F.softmax(logits_v, dim=1)
                probs = probs_v.data.cpu().numpy()
                action = np.argmax(probs)
                if action == 0 and pre_action == action:  # Noop
                    noop_action_count += 1
                    if noop_action_count > 30:
                        break
                else:
                    noop_action_count = 0
                pre_action = action
                obs, reward, done, trunc, _ = env.step(action)
                total_reward += reward
                if done or trunc:
                    break
    return total_reward / episodes


def optimized_states_preprocessor(states):
    """
    Convert list of states into the form suitable for model.
    :param states: list of numpy arrays with states
    :return: torch.Tensor
    """
    if len(states) == 1:
        np_states = np.expand_dims(states[0], 0)
    else:
        np_states = np.asarray([np.asarray(s) for s in states])
    return torch.from_numpy(np_states)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--cuda", default=True, action="store_true", help="Enable cuda")
    parser.add_argument("-n", "--name", default="kaboom", required=False, help="Name of the run")
    parser.add_argument("--kan_act", default=False, help="kan 是否使用激活函数")
    # parser.add_argument("--seed", type=int, default=42, help="Random seed")
    args = parser.parse_args()
    device = common.select_device(args=args)

    save_path = os.path.join("saves", "a2c-conv" + args.name + "_kan_act" if args.kan_act else "")
    if not os.path.exists(save_path):
        os.makedirs(save_path)
    envs = [common.wrap_dqn(gym.make("ALE/Kaboom-v5", obs_type='rgb', frameskip=4, repeat_action_probability=0.0), episodic_life=False) for _ in range(NUM_ENVS)]
    test_env = common.wrap_dqn(gym.make("ALE/Kaboom-v5", obs_type='rgb', frameskip=4, repeat_action_probability=0.0), episodic_life=False)
    writer = SummaryWriter(comment="-a2c-conv_" + args.name + "_kan_act" if args.kan_act else "")
    # common.set_seed(args.seed)

    net = model.AtariA2C(envs[0].observation_space.shape, envs[0].action_space.n, args.kan_act).to(device)
    print(net)

    agent = ptan.agent.PolicyAgent(lambda x: net(x)[0], apply_softmax=True, device=device, preprocessor=optimized_states_preprocessor)
    exp_source = ptan.experience.ExperienceSourceFirstLast(envs, agent, gamma=GAMMA, steps_count=REWARD_STEPS)
    optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE, eps=1e-3)
    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=50000, gamma=0.9)

    batch = []
    best_reward = 0
    frame_idx = 0
    train_count = 0
    start_idx = 0

    if os.path.exists(save_path) and len(os.listdir(save_path)) > 0:
        # 增加加载模型的代码
        checkpoints = sorted(filter(lambda x: "epoch" in x, os.listdir(save_path)), key=lambda x: int(x.split('_')[2].split('.')[0]))
        checkpoint = torch.load(os.path.join(save_path, checkpoints[-1]), map_location=device, weights_only=False)
        frame_idx = checkpoint['frame_idx']
        start_idx = checkpoint['start_idx']
        # train_count = checkpoint['train_count']
        net.load_state_dict(checkpoint['net'])
        optimizer.load_state_dict(checkpoint['optimizer'])
        scheduler.load_state_dict(checkpoint['scheduler'])
        print("加载模型成功")
        print("scheduler step:", scheduler.last_epoch)
        print("learning rate:", scheduler.get_last_lr()[0])
        train_count = scheduler.last_epoch
    # 打印学习率大小
    print("Learning Rate:", scheduler.get_last_lr()[0])

    with common.RewardTracker(writer, stop_reward=10000) as tracker:
        with ptan.common.utils.TBMeanTracker(writer, batch_size=10) as tb_tracker:
            for step_idx, exp in enumerate(exp_source):
                batch.append(exp)

                new_rewards = exp_source.pop_total_rewards()
                if new_rewards:
                    if tracker.reward(new_rewards[0], step_idx + start_idx):
                        break

                if len(batch) < BATCH_SIZE:
                    continue


                states_v, actions_t, vals_ref_v = unpack_batch(batch, net, device=device)
                batch.clear()

                optimizer.zero_grad()
                logits_v, value_v = net(states_v)
                loss_value_v = F.mse_loss(value_v.squeeze(-1), vals_ref_v)

                log_prob_v = F.log_softmax(logits_v, dim=1)
                adv_v = vals_ref_v - value_v.squeeze(-1).detach()
                log_prob_actions_v = adv_v * log_prob_v[range(BATCH_SIZE), actions_t]
                loss_policy_v = -log_prob_actions_v.mean()

                prob_v = F.softmax(logits_v, dim=1)
                entropy_loss_v = ENTROPY_BETA * (prob_v * log_prob_v).sum(dim=1).mean()

                loss_policy_v.backward(retain_graph=True)
                grads = np.concatenate([p.grad.data.cpu().numpy().flatten()
                                        for p in net.parameters()
                                        if p.grad is not None])

                loss_v = entropy_loss_v + loss_value_v
                loss_v.backward()
                nn_utils.clip_grad_norm_(net.parameters(), CLIP_GRAD)
                optimizer.step()
                loss_v += loss_policy_v
                frame_idx += 1
                # scheduler.step()
                train_count += 1

                if frame_idx % 200 == 0:
                    # Test the model
                    net.eval()
                    test_reward = test_model(test_env, net, device=device, episodes=2)
                    net.train()
                    print(f"Test reward: {test_reward:.2f}")
                    common.save_best_model(test_reward, net.state_dict(), save_path, "a2c-best", keep_best=10)

                if frame_idx % SAVE_ITERS == 0:
                    checkpoint = {
                        "net": net.state_dict(),
                        "optimizer": optimizer.state_dict(),
                        "frame_idx": frame_idx,
                        "start_idx": step_idx + start_idx,
                        "scheduler": scheduler.state_dict(),
                        "train_count": train_count
                    }
                    common.save_checkpoints(frame_idx, checkpoint, save_path, "a2c", keep_last=5)


                tb_tracker.track("advantage",       adv_v, step_idx + start_idx)
                tb_tracker.track("values",          value_v, step_idx + start_idx)
                tb_tracker.track("batch_rewards",   vals_ref_v, step_idx + start_idx)
                tb_tracker.track("loss_entropy",    entropy_loss_v, step_idx + start_idx)
                tb_tracker.track("loss_policy",     loss_policy_v, step_idx + start_idx)
                tb_tracker.track("loss_value",      loss_value_v, step_idx + start_idx)
                tb_tracker.track("loss_total",      loss_v, step_idx + start_idx)
                tb_tracker.track("grad_l2",         np.sqrt(np.mean(np.square(grads))), step_idx + start_idx)
                tb_tracker.track("grad_max",        np.max(np.abs(grads)), step_idx + start_idx)
                tb_tracker.track("grad_var",        np.var(grads), step_idx + start_idx)